--- title: "03-Redis 应用实战" created: 2026-08-31 tags: - 项目筑基 --- # Redis 应用实战 > Redis 系统讲解第二篇:**怎么把 Redis 用对**。01 篇已经给出缓存三兄弟的速查结论,这一篇把每个解法讲透——为什么有效、什么条件下失效、代码长什么样。 ## 缓存读写策略全景 三种经典读写策略,不是三选一的平级选项,而是各有定位: | 策略 | 流程 | 适用 | 代价 | | --- | --- | --- | --- | | **Cache Aside**(旁路缓存) | 读:先缓存后库;写:**先更库再删缓存** | 最通用,日常默认 | 写后短时间内缓存与库可能短暂不一致 | | Read/Write Through | 应用只跟缓存层说话,缓存层自己管库 | 需要缓存服务支持,少见于手写 | 实现复杂 | | Write Behind | 写只进缓存,异步批量刷库 | 超高写入(点赞数) | **缓存挂了丢数据**,慎用 | **为什么是"删缓存"而不是"更新缓存"**:并发两个写,更新缓存的顺序可能与数据库提交顺序相反,把旧值写回缓存且再也不会被修正;删掉让下次读自然回源,天然收敛。 **为什么是"先更库"而不是"先删缓存"**:先删后更的窗口里,另一个读请求会从库里拿旧值回填缓存,旧值会一直住到下次过期;先更库再删,最坏也只是短暂不一致。(书里的严谨版本:还有"延迟双删"和"订阅 binlog 异步删"两种增强,见下) | 一致性方案 | 做法 | 适用 | | --- | --- | --- | | 先更库再删缓存 | 日常默认 | 绝大多数场景 | | 延迟双删 | 删缓存 → 更库 → 睡几百毫秒再删一次 | 读多写多、怕回填旧值 | | 订阅 binlog 异步删 | Canal 等中间件监听 MySQL binlog 触发删缓存 | 要求最终一致性的中大型系统 | ## 缓存三兄弟:解法的适用条件 01 篇是结论表,这里记"为什么": - **穿透(查不存在的数据)**:布隆过滤器的原理是"用多个 hash 位标记存在性"——说"不存在"一定不存在,说"可能存在"要再查库确认。所以它能**挡掉绝大多数恶意 id**,代价是误判率(存在性判断有假阳性)与不能删除元素。 - **击穿(热点 key 过期瞬间)**:互斥锁的代码骨架——拿到锁的那个请求去回源重建缓存,其他人短暂等待: ```python val = r.get(key) if val is None: # 只放一个请求进去重建;setnx 成功者干活,其余人睡 50ms 重试 if r.set(f"lock:{key}", "1", nx=True, ex=10): data = db.query(key) r.set(key, serialize(data), ex=3600) r.delete(f"lock:{key}") else: time.sleep(0.05) return get_from_cache_or_db(key) # 递归重试 ``` - 逻辑过期的思路相反:**缓存永不物理过期**,value 里带过期时间字段,读到"已过期"的再异步重建——适合重建成本极高的热点数据。 - **雪崩(大批 key 同时过期 / Redis 整体宕机)**:过期时间加随机是工程上最便宜的一招;多级缓存(本地 Caffeine/进程内 dict → Redis → DB)是架构级的答案;宕机靠哨兵/集群 + 限流降级兜底。 ## 分布式锁:从玩具到能上生产 01 篇的 `SET NX EX` 是玩具版,能上生产还差三件事: 1. **锁误删**:A 拿锁后超时自动释放,B 抢到锁,A 干完活 DEL 把 B 的锁删了。解法:value 存唯一 token,删锁前用 **Lua 脚本校验"是自己的锁才删"**(GET+DEL 必须原子): ```lua if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end ``` 2. **锁续期**:业务没跑完锁先过期。Redisson 的看门狗(Java)自动续期;Python 生态用 `redis-py` + 定时续期线程,或 `redlock-py`。 3. **Redlock 的争议**:多节点 Redlock 算法在分布式系统界(Martin Kleppmann 的著名质疑)有安全性争议——**单实例锁 + 容忍极小概率失效,对绝大多数业务足够**;要强一致锁,考虑用 ZooKeeper/etcd。 ## 限流:三种算法的 Redis 实现 | 算法 | Redis 实现 | 特点 | | --- | --- | --- | | 固定窗口 | `INCR` + `EXPIRE`,超过阈值拒绝 | 简单;窗口边界处可能放过 2 倍流量 | | 滑动窗口 | ZSet 存时间戳,`ZREMRANGEBYSCORE` 清旧 + `ZCARD` 计数 | 精确,内存随请求量涨 | | 令牌桶 | Lua 脚本里按时间生成令牌 + 扣减 | 允许突发,最通用 | 固定窗口最小骨架: ```python n = r.incr(f"rate:{uid}:{int(time.time()) // 60}") if n == 1: r.expire(f"rate:{uid}:{int(time.time()) // 60}", 65) if n > 100: raise TooManyRequests() ``` > 💡 真正的限流器 Lua 必须把"读-判-写"包成原子操作,否则并发下计数失真——和分布式锁删锁是同一个道理:**多步操作在 Redis 里要用 Lua 合成原子**。 ## 秒杀骨架(把上面全部串起来) ```text 1. 预热:把库存数 SET 进 Redis(SET stock:sku1 100) 2. 削峰:令牌桶/滑动窗口限流,拦掉绝大部分流量 3. 扣减:Lua 脚本"判断库存 > 0 就 DECR"原子执行(防超卖) 4. 异步:扣减成功者发消息进 Stream,后台慢慢创建订单落 MySQL ``` 单靠 MySQL 扛秒杀是灾难,Redis 扛流量、MySQL 扛最终数据——这是"为什么后端必学 Redis"的最直观答案。 --- ⬅️ [[02-Redis 核心数据结构与命令|02-Redis 核心数据结构与命令]] 🏠 [[00-数据库|00-数据库]] ➡️ [[04-Redis 持久化与高可用|04-Redis 持久化与高可用]]